const std = @import("std");
const log = std.log.scoped(.syscall);
const trace = std.log.scoped(.trace);
const os = @import("os.zig");
const Proc = os.proc.Proc;

pub const sysfile = @import("syscall/file.zig");
pub const sysproc = @import("syscall/proc.zig");
pub const sysnet = @import("syscall/net.zig");

/// The enum type extracted from the `SysCall` union, each variant corresponds to a unique number.
pub const SysTag = std.meta.Tag(SysCall);

/// Union representing system calls, where each variant holds the function signature for a specific call.
pub const SysCall = union(enum) {
    /// 0: terminate the system
    shutdown: *const fn (code: u16) callconv(.c) noreturn,
    /// 1: fork a new process
    fork: *const fn () callconv(.c) isize,
    /// 2: terminate current process
    exit: *const fn (status: i32) callconv(.c) noreturn,
    /// 3: wait for a child process to exit
    wait: *const fn (status: ?*i32) callconv(.c) isize,
    /// 4: create a pipe
    pipe: *const fn (fds: *[2]fd_t) callconv(.c) c_int,
    /// 5: read data from a file descriptor into a buffer
    read: *const fn (fd: fd_t, buf: [*]u8, size: usize) callconv(.c) isize,
    /// 6: send a signal to a process to terminate it
    kill: *const fn (pid: pid_t) callconv(.c) c_int,
    /// 7: execute a new program
    exec: *const fn (path: c_str, argv: [*:null]const ?c_str) callconv(.c) c_int,
    /// 8: get status information about a file
    fstat: *const fn (fd: fd_t, stat: *os.fs.Inode.Stat) callconv(.c) c_int,
    /// 9: change the current working directory
    chdir: *const fn (path: c_str) callconv(.c) c_int,
    /// 10: duplicate an existing file descriptor
    dup: *const fn (fd: fd_t) callconv(.c) isize,
    /// 11: get the process ID of the calling process
    getpid: *const fn () callconv(.c) isize,
    /// 12: increase or decrease the process data segment size
    sbrk: *const fn (increment: isize) callconv(.c) [*]u8,
    /// 13: pause for a specified number of ticks
    pause: *const fn (nticks: usize) callconv(.c) c_int,
    /// 14: get the system uptime in clock ticks
    uptime: *const fn () callconv(.c) usize,
    /// 15: open or create a file
    open: *const fn (path: c_str, flag: os.fs.O) callconv(.c) isize,
    /// 16: write data from a buffer to a file descriptor
    write: *const fn (fd: fd_t, buf: [*]const u8, size: usize) callconv(.c) isize,
    /// 17: create a device file
    mknod: *const fn (path: c_str, major: u16, minor: u16) callconv(.c) c_int,
    /// 18: remove a hard link
    unlink: *const fn (path: c_str) callconv(.c) c_int,
    /// 19: create a hard link to an existing file
    link: *const fn (file: c_str, link: c_str) callconv(.c) c_int,
    /// 20: create a new directory
    mkdir: *const fn (path: c_str) callconv(.c) c_int,
    /// 21: close a file descriptor
    close: *const fn (fd: fd_t) callconv(.c) c_int,
    /// 22: create a symbolic link
    symlink: *const fn (file: c_str, link: c_str) callconv(.c) c_int,
    /// 23: check accessibility of a file
    access: *const fn (path: c_str) callconv(.c) bool,
    /// 24: get the current working directory of the process
    getcwd: *const fn (buf: [*]u8, size: usize) callconv(.c) c_int,
    /// 25: enable or disable tracing of system calls
    trace: *const fn (mask: usize) callconv(.c) c_int,
    /// 26: send a network packet
    send: *const fn (sport: u16, dst: u32, dport: u16, buf: [*]u8, size: usize) callconv(.c) c_int,
    /// 27: receive a network packet
    recv: *const fn (dport: u16, src: *u32, sport: *u16, buf: [*]u8, size: usize) callconv(.c) c_int,
    /// 28: bind a network port for receiving packets
    bind: *const fn (port: u16) callconv(.c) c_int,
    /// 29: unbind a previously bound network port
    unbind: *const fn (port: u16) callconv(.c) c_int,
    /// 30: reposition read/write file offset
    lseek: *const fn (fd: fd_t, offset: off_t, whence: os.fs.SeekMode) callconv(.c) off_t,

    pub const fd_t = os.fs.fd_t;
    pub const off_t = os.fs.off_t;
    pub const pid_t = os.proc.pid_t;

    /// C-compatible null-terminated string pointer type for userspace syscall interface.
    /// This type is used when userspace passes raw C strings to system calls.
    pub const c_str = [*:0]const u8;

    /// Kernel path type representing a null-terminated, owned string slice.
    /// Used for safe string handling within the kernel, ensuring proper null termination.
    pub const path_t = [:0]const u8;
};

/// Safely fetches and converts the nth system call argument to the requested type.
/// This function handles both integer types and any other type that has a compatible
/// bit representation with integers.
fn argint(p: *Proc, comptime T: type, comptime n: u8) !T {
    const raw: usize = p.ctx.uproc.arg(n);
    const signed = switch (@typeInfo(T)) {
        .int => |info| info.signedness == .signed,
        else => false,
    };
    const Int = if (signed) isize else std.meta.Int(.unsigned, @bitSizeOf(T));
    const int: Int = if (signed) @bitCast(raw) else try os.lib.intCast(Int, raw);
    return if (signed) try os.lib.intCast(T, int) else os.lib.typeFromInt(T, int);
}

/// Retrieve an argument as a pointer.
fn argaddr(p: *Proc, Ptr: type, comptime n: u8) !Ptr {
    const addr = p.ctx.uproc.arg(n);
    const allow_zero = switch (@typeInfo(Ptr)) {
        .optional => true,
        .pointer => |info| info.is_allowzero,
        else => false,
    };
    if (!allow_zero and addr == 0) return error.InvalidAddress;

    // Doesn't check for legality, since copyin/copyout will do that.
    @setRuntimeSafety(false);
    return @ptrFromInt(addr);
}

/// Fetch the nth word-sized system call argument as a null-terminated string.
/// Copies into buf, at most max.
/// Returns string length if OK (including nul), panic if error.
fn argstr(p: *Proc, comptime n: u8, buf: []u8) ![:0]const u8 {
    const addr: usize = p.ctx.uproc.arg(n);
    return p.ctx.uproc.fetchStr(addr, buf);
}

/// The mandatory initial arguments for all syscalls, starting with the current process pointer.
const syscall_base_args: []const type = &.{*Proc};

/// Constructs the complete argument tuple type for a syscall function, prepending the process pointer.
/// This helper creates a tuple that combines the mandatory `*Proc` parameter with the syscall's specific arguments.
fn ArgsTuple(comptime Func: type) type {
    const func_args = std.meta.fields(std.meta.ArgsTuple(Func));

    var type_list: [syscall_base_args.len + func_args.len]type = undefined;
    @memcpy(type_list[0..syscall_base_args.len], syscall_base_args);

    inline for (func_args, 0..) |field, i| {
        // Raw C strings from userspace are transformed into managed kernel string slices
        const arg_type = if (field.type == SysCall.c_str) SysCall.path_t else field.type;
        type_list[syscall_base_args.len + i] = arg_type;
    }

    return std.meta.Tuple(&type_list);
}

/// Extracts and converts system call arguments from the current process into a typed tuple.
fn sysargs(comptime Func: type, arena: std.mem.Allocator) !ArgsTuple(Func) {
    const p = os.proc.current().?;
    const Args = ArgsTuple(Func);
    var arg_list: Args = undefined;
    arg_list[0] = p;

    inline for (std.meta.fields(Args)[syscall_base_args.len..], 0..) |field, n| {
        @field(arg_list, field.name) = switch (@typeInfo(field.type)) {
            .int, .@"enum" => try argint(p, field.type, n),
            .pointer => switch (field.type) {
                SysCall.path_t => str: {
                    const buf = try arena.alloc(u8, os.fs.MAXPATH);
                    break :str try argstr(p, n, buf);
                },
                else => try argaddr(p, field.type, n),
            },

            .optional => |info| if (@typeInfo(info.child) == .pointer)
                try argaddr(p, field.type, n)
            else
                @compileError("unsupported optional type: " ++ @typeName(field.type)),

            else => if (comptime std.meta.containerLayout(field.type) == .@"packed")
                try argint(p, field.type, n)
            else
                @compileError("unsupported argument type: " ++ @typeName(field.type)),
        };
    }
    return arg_list;
}

/// Function type for syscall wrappers, which return a result or error code as `usize`.
const SysCallFn = fn () anyerror!usize;

/// Generates a wrapper function for a syscall, handling argument fetching and result conversion.
fn sysCall(comptime name: []const u8) SysCallFn {
    const func = for (.{ sysproc, sysfile, sysnet }) |container| {
        if (@hasDecl(container, name)) break @field(container, name);
    } else @compileError("unknown syscall");

    const FnReturnType = if (@typeInfo(@TypeOf(func)).@"fn".return_type) |return_type| return_type else void;
    comptime std.debug.assert(@typeInfo(FnReturnType) == .error_union);
    const PayloadType = @typeInfo(FnReturnType).error_union.payload;

    return struct {
        fn call() !usize {
            var arena: std.heap.ArenaAllocator = .init(std.heap.page_allocator);
            defer arena.deinit();
            const allocator = arena.allocator();

            const args = try sysargs(std.meta.Child(@FieldType(SysCall, name)), allocator);
            const result: PayloadType = try @call(.auto, func, args);

            return switch (@typeInfo(PayloadType)) {
                .void => 0,
                .bool => @intFromBool(result),
                .int => |info| switch (info.signedness) {
                    .signed => @as(usize, @bitCast(result)),
                    .unsigned => result,
                },
                else => unreachable,
            };
        }
    }.call;
}

/// An array type that maps syscall enum tags to their handler function pointers.
const SysArray = std.EnumArray(SysTag, *const SysCallFn);

/// The system call dispatch table, initialized at compile-time with all handler functions.
const functions: SysArray = fns: {
    var fns: SysArray = .initUndefined();
    for (std.meta.fields(SysTag)) |call| {
        fns.set(@field(SysTag, call.name), sysCall(call.name));
    }
    break :fns fns;
};

pub fn syscall() void {
    const p = os.proc.current().?;
    const reg = &p.ctx.uproc.trapframe.reg;
    const num: usize = reg.a7;
    const err_value: usize = std.math.maxInt(usize);

    // validate syscall number and convert to enum tag
    const call: SysTag = if (num < SysArray.len) @enumFromInt(num) else {
        log.warn("{} {s}: unknown syscall {}", .{ p.pid, p.name.get(), num });
        reg.a0 = err_value;
        return;
    };

    // lookup the system call function, call it, and store its return value in a0
    reg.a0 = functions.get(call)() catch |err| ret: {
        log.warn("{} {t} failed: {t}", .{ p.pid, call, err });
        break :ret err_value;
    };

    // log traced syscalls if enabled for this call number
    if (p.ctx.uproc.mask & (@as(usize, 1) << @intCast(num)) > 0) {
        trace.info("{}: syscall {t} -> {}", .{ p.pid, call, reg.a0 });
    }
}
